package io.github.wslxm.springbootplus2.utils;

import cn.hutool.core.util.IdUtil;
import io.github.wslxm.springbootplus2.constant.CacheKey;
import io.github.wslxm.springbootplus2.core.config.error.ErrorException;
import io.github.wslxm.springbootplus2.core.result.Result;
import io.github.wslxm.springbootplus2.core.result.ResultType;
import io.github.wslxm.springbootplus2.core.utils.XjIpUtil;
import io.github.wslxm.springbootplus2.core.utils.bean.XjSpringContextUtil;
import io.github.wslxm.springbootplus2.starter.redis.util.RedisUtil;
import io.github.wslxm.springbootplus2.utils.model.JwtUser;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Set;

/***
 *   jwt 工具类
 *   <P>
 *      使用Deflater 压缩数据后再放到 jwt存储数据，以保证不出现 header 大小问题(不能超过8kb) max-http-header-size
 *      jwt工具包会把最后的数据进行base64编码返回
 *   </P>
 * @author 王松
 * @mail 1720696548@qq.com
 * @date 2020/7/5 0005 19:13
 */
@SuppressWarnings("all")
@Slf4j
public class JwtUtil {

    /**
     * TOKEN KEY 值
     */
    public static String TOKEN = "token";
    /**
     * 登录类型 0=管理端 | 1=用户端 | 2-扩展端1 | 3-扩展端2 | 4-扩展端3 | 更多自定义
     */
    public static final Integer[] userType = {0, 1, 2, 3, 4};


    private static RedisUtil redisUtil = null;

    private static void init() {
        if (redisUtil == null) {
            redisUtil = (RedisUtil) XjSpringContextUtil.getBean("redisUtil");
        }
    }

    private static HttpServletRequest getHttpServletRequest() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        return request;
    }


    /**
     * 生成 token
     *
     * @param jwtUser  用户信息 只需要设置  type userId fullName roleCodes 数据
     * @param response
     * @return java.lang.String
     * @date 2020/7/6 0006 9:26
     */
    public static String createToken(JwtUser jwtUser, HttpServletResponse response) {
        init();
        // 剔除其他账号登录状态
        // removeLoginState(jwtUser.getType(), jwtUser.getUserId());
        String token = setToken(jwtUser);
        response.setHeader(TOKEN, token);
        return token;
    }


    /**
     * 获取用户信息（自动区分登录类型--生成jwt时指定, jwtUser的type ）
     * <p>
     * 此方法用于在项目中的任意地方获取用户信息，如果在登录授权验证中未过期，但在项目使用时过期, 会在这里抛出自定义异常，程序中无需判断
     * </P>
     *
     * @param token
     * @return
     */
    public static JwtUser getJwtUser(HttpServletRequest request) {
        init();
        Result<JwtUser> jwtUser2 = getJwtUserR(request, null);
        if (!jwtUser2.getCode().equals(ResultType.SYS_SUCCESS.getValue())) {
            throw new ErrorException(jwtUser2.getCode(), jwtUser2.getMsg());
        }
        return jwtUser2.getData();
    }


    /**
     * 获取登录信息，如过 token无效过期等，会进入对应的异常信息中返回
     * <p>
     * 1、此方法用于权限验证, aop中，日志中获取用户信息, 注意返回的 Result，如果出现异常,过期等信息不会直接抛出, 将返回到R 中
     * 2、此方法可在业务代码中判断当前是否登录( Result.getCode == 200 表示登录/ 其他情况表示未登录)
     * 示例代码：
     * Result<JwtUser> jwtUserR = JwtUtil.getJwtUserR(request, response);
     * Boolean isLogin = jwtUserR.getCode().equals(ResultType.SYS_SUCCESS.getValue()) ? true : false;
     * if (isLogin) {
     * String userId = jwtUserR.getData().getUserId();
     * }
     * </p>
     *
     * @param request
     * @param response
     * @return
     */
    public static Result<JwtUser> getJwtUserR(HttpServletRequest request, HttpServletResponse response) {
        init();
        // 判断是否传递tokne
        String token = null;
        try {
            token = request.getHeader(TOKEN);
        } catch (Exception e) {
            return Result.error(ResultType.GATEWAY_NO_TOKEN);
        }
        if (token == null || token == "") {
            return Result.error(ResultType.GATEWAY_NO_TOKEN);
        }
        // 判断登录 token 是否存在
        String loginTokenKey = getTokenKeyPrefix() + token;
        if (!redisUtil.hasKey(loginTokenKey)) {
            return Result.error(ResultType.GATEWAY_LOGIN_EXPIRED);
        }
        // 获取用户信息
        JwtUser jwtUser = (JwtUser) redisUtil.get(loginTokenKey);
        if (jwtUser == null) {
            return Result.error(ResultType.GATEWAY_LOGIN_EXPIRED);
        }
        // 更新 tokne 有效期
        updateToken(token, jwtUser);
        // 返回用户信息
        return Result.success(jwtUser);
    }


    /**
     * 删除指定用户的登录状态
     *
     * @param userType 用户类型- 0-管理端 1-用户端 (传类型是防止多个登录端的用户id有相同的)
     * @param userId   用户id
     * @param token    当前登录 token (同时在多处登录时: 传递了token,则只删除当前登录, 不传递则删除当前 userId 的所有登录)
     * @return
     */
    public static void removeLoginState(Integer userType, String userId, String token) {
        init();
        if (StringUtils.isNotBlank(token)) {
            String userKey = getUserKeyPrefix(userType, userId) + token;
            String tokenKey = getTokenKeyPrefix() + token;
            redisUtil.delete(userKey);
            redisUtil.delete(tokenKey);
        } else {
            // 获取当前用户的所有登录 userKey/tokenKey, 并进行删除
            String userKeyPrefix = getUserKeyPrefix(userType, userId);
            Set<String> keys = redisUtil.getKeysByPrefix(userKeyPrefix);
            for (String userKey : keys) {
                String tokenKey = redisUtil.get(userKey).toString();
                redisUtil.delete(userKey);
                redisUtil.delete(tokenKey);
            }
        }
    }

    /**
     * 更新登录用户的数据 (用户信息发送变化时, 角色权限可以实时更新)
     *
     * @param jwtUser 只需要设置  type userId fullName roleCodes 数据
     * @return
     */
    public static boolean updateTokenValue(Integer userType, String userId, JwtUser jwtUser) {
        String userKeyPrefix = JwtUtil.getUserKeyPrefix(userType, userId);
        Set<String> loginUserKeys = redisUtil.getKeysByPrefix(userKeyPrefix);
        // 更新缓存数据
        for (String loginUserKey : loginUserKeys) {
            String loginTokenKey = redisUtil.get(loginUserKey).toString();
            long expiration = redisUtil.getExpire(loginTokenKey);
            //
            HttpServletRequest request = getHttpServletRequest();
            jwtUser.setIp(XjIpUtil.getIp(request));
            jwtUser.setUserAgent(request.getHeader("User-Agent"));
            jwtUser.setExpiration(Integer.parseInt(expiration + ""));
            redisUtil.set(loginTokenKey, jwtUser,  expiration);
        }
        return true;
    }


    /**
     * 设置 token 到 redis 中
     */
    private static String setToken(JwtUser jwtUser) {
        HttpServletRequest request = getHttpServletRequest();
        jwtUser.setIp(XjIpUtil.getIp(request));
        jwtUser.setUserAgent(request.getHeader("User-Agent"));
        String token = IdUtil.simpleUUID();
        String loginTokenKey = getTokenKeyPrefix() + token;
        redisUtil.set(loginTokenKey, jwtUser, jwtUser.getExpiration() * 60);
        //
        String loginUserKey = getUserKeyPrefix(jwtUser.getType(), jwtUser.getUserId()) + token;
        redisUtil.set(loginUserKey, loginTokenKey, jwtUser.getExpiration() * 60);
        return token;
    }


    /**
     * 更新 redis 中当前用户 token 的有效期
     * 更新规则: 当token的有效期使用超过了 1/10 时, 自动续期为最长时效
     */
    private static void updateToken(String token, JwtUser jwtUser) {
        String loginTokenKey = getTokenKeyPrefix() + token;
        long expire = redisUtil.getExpire(loginTokenKey);
        long refreshTime = (long) ((Double.parseDouble(jwtUser.getExpiration() + "") / 10) * 60);
        if (((jwtUser.getExpiration() * 60) - refreshTime) > expire) {
            redisUtil.set(loginTokenKey, jwtUser, jwtUser.getExpiration() * 60);
            //
            String loginUserKey = getUserKeyPrefix(jwtUser.getType(), jwtUser.getUserId()) + token;
            redisUtil.set(loginUserKey, loginTokenKey, jwtUser.getExpiration() * 60);
        }
    }


    /**
     * 获取当前登录用户的 token 获取 redis 的 tokenKey 前缀
     *
     * @param token
     * @return
     */
    private static String getTokenKeyPrefix() {
        return redisUtil.getPrefix() + CacheKey.LOGIN_TOKEN + "::";
    }

    /**
     * 通过当前登录用户的用户id 获取 redis 的 在线用户 key 前缀
     *
     * @param userType 登录类型 - 0管理端 1用户端
     * @param userId
     * @return
     */
    private static String getUserKeyPrefix(Integer userType, String userId) {
        return redisUtil.getPrefix() + CacheKey.LOGIN_USER + "_" + userType + "::" + userId + "::";
    }
}